package br.com.winesys.geta.editor;

import java.awt.Component;
import java.awt.Container;
import java.awt.Cursor;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.HierarchyBoundsListener;
import java.awt.event.HierarchyEvent;
import java.awt.event.HierarchyListener;
import java.awt.event.MouseEvent;
import java.awt.geom.Area;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.swing.JComponent;
import javax.swing.JLayeredPane;
import javax.swing.RootPaneContainer;
import javax.swing.SwingUtilities;

/**
 * Provide a method for consistently augmenting the appearance of a given
 * component by painting something on it <i>after</i> the component itself gets
 * painted. If not explicitly removed via {@link #dispose}, an instance of this
 * object will live as long as its target component.
 * <p>
 * By default, the decorator matches the location and size of the decorated
 * component, but the bounds can be adjusted by overriding
 * {@link #getDecorationBounds()}. The {@link #synch()} method should be called
 * whenever the bounds returned by {@link #getDecorationBounds()} would change.
 * <p>
 * The decoration is clipped to the bounds set on the decoration, which does not
 * necessarily need to be the same as the decorated component's bounds. The
 * decoration may extend beyond the decorated component bounds, or it may be
 * reduced to a smaller region.
 */
public abstract class AbstractComponentDecorator {

	public static final Rectangle DEFAULT_BOUNDS = null;
	public static final int TOP = 0;
	// Disabled for now, since the "bottom" layered pane position doesn't work
	// as advertised.	
	//private static final int BOTTOM = -1;
	/**
	 * Account for the difference between the decorator actual origin and the
	 * logical origin we want to pass to the {@link #paint} method.
	 */
	private Point originOffset = new Point(0, 0);
	private Painter painter;
	private JComponent component;
	private Container parent;
	private Component layerRoot;
	private Listener listener;
	private int layerOffset;
	private int position;
	private Rectangle bounds;

	public AbstractComponentDecorator(JComponent c) {
		this(c, 1);
	}

	/**
	 * Create a decorator for the given component, indicating the layer offset
	 * from the target component. Negative values mean the decoration is painted
	 * <em>before</em> the target component is painted.
	 */
	public AbstractComponentDecorator(JComponent c, int layerOffset) {
		this(c, layerOffset, TOP);
	}

	/**
	 * Create a decorator with the given position within its layer. Use
	 * {@link #TOP} to cover other decorations, or {@link #BOTTOM} to be covered
	 * by other decorations.
	 * <p>
	 * WARNING: BOTTOM doesn't currently work, probably a JLayeredPane bug in
	 * either the code or documentation.
	 * 
	 * @see JLayeredPane
	 */
	public AbstractComponentDecorator(JComponent c, int layerOffset, int position) {
		this.component = c;
		this.layerOffset = layerOffset;
		this.position = position;
		this.bounds = DEFAULT_BOUNDS;
		this.parent = c.getParent();
		this.painter = new Painter();
		this.listener = new Listener();
		this.component.addHierarchyListener(this.listener);
		this.component.addHierarchyBoundsListener(this.listener);
		this.component.addComponentListener(this.listener);
		attach();
	}

	/**
	 * Set the text to be displayed when the mouse is over the decoration.
	 * 
	 * @see JComponent#setToolTipText(String)
	 */
	public void setToolTipText(String text) {
		this.painter.setToolTipText(text);
	}

	/**
	 * Return the currently set default tooltip text.
	 * 
	 * @see JComponent#setToolTipText
	 */
	public String getToolTipText() {
		return this.painter.getToolTipText();
	}

	/**
	 * Indicate whether the decoration is visible. The decoration may be clipped
	 * by ancestor scroll panes or by being moved outside if the visible region
	 * of its parent window.
	 */
	public boolean isVisible() {
		return this.painter.isVisible();
	}

	/** Use this to change the visibility of the decoration. */
	public void setVisible(boolean visible) {
		this.painter.setVisible(visible);
	}

	protected void attach() {
		if (this.layerRoot != null) {
			this.layerRoot.removePropertyChangeListener(this.listener);
			this.layerRoot = null;
		}
		RootPaneContainer rpc = (RootPaneContainer) SwingUtilities.getAncestorOfClass(RootPaneContainer.class, this.component);
		if (rpc != null
				&& SwingUtilities.isDescendingFrom(this.component, rpc.getLayeredPane())) {
			JLayeredPane lp = rpc.getLayeredPane();
			Component layeredChild = this.component;
			int layer = JLayeredPane.DRAG_LAYER.intValue();
			if (this instanceof BackgroundPainter) {
				layer = ((BackgroundPainter) this).layer;
				this.painter.setDecoratedLayer(layer);
			} else if (layeredChild == lp) {
				// Is the drag layer the best layer to use when decorating
				// the layered pane?
				this.painter.setDecoratedLayer(layer);
			} else {
				while (layeredChild.getParent() != lp) {
					layeredChild = layeredChild.getParent();
				}
				int base = lp.getLayer(layeredChild);
				// NOTE: JLayeredPane doesn't properly repaint an overlapping
				// child when an obscured child calls repaint() if the two
				// are in the same layer, so we use the next-higher layer
				// instead of simply using a different position within the
				// layer.
				layer = base + this.layerOffset;
				if (this.layerOffset < 0) {
					BackgroundPainter bp = (BackgroundPainter) lp.getClientProperty(BackgroundPainter.key(base));
					if (bp == null) {
						bp = new BackgroundPainter(lp, base);
					}
				}
				this.painter.setDecoratedLayer(base);
				this.layerRoot = layeredChild;
				this.layerRoot.addPropertyChangeListener(this.listener);
			}
			lp.add(this.painter, new Integer(layer), this.position);
		} else {
			// Always detach when the target component's window is null
			// or is not a suitable container,
			// otherwise we might prevent GC of the component
			Container container = this.painter.getParent();
			if (container != null) {
				container.remove(this.painter);
			}
		}
		// Track size changes in the decorated component's parent
		if (this.parent != null) {
			this.parent.removeComponentListener(this.listener);
		}
		this.parent = this.component.getParent();
		if (this.parent != null) {
			this.parent.addComponentListener(this.listener);
		}
		synch();
	}

	/**
	 * Ensure the size of the decorator matches the current decoration bounds
	 * with appropriate clipping to viewports.
	 */
	protected void synch() {
		Container painterParent = this.painter.getParent();
		if (painterParent != null) {
			Rectangle decorated = getDecorationBounds();
			Rectangle clipRect = clipDecorationBounds(decorated);

			Point pt = SwingUtilities.convertPoint(this.component,
												clipRect.x, clipRect.y,
												painterParent);
			if (clipRect.width <= 0 || clipRect.height <= 0) {
				setPainterBounds(-1, -1, 0, 0);
				setVisible(false);
			} else {
				setPainterBounds(pt.x, pt.y, clipRect.width, clipRect.height);
				setVisible(true);
			}
			painterParent.repaint();
		}
	}

	/**
	 * Adjust the painting offsets and size of the decoration to account for
	 * ancestor clipping. This might be due to scroll panes or having the
	 * decoration lie outside the parent layered pane.
	 */
	protected Rectangle clipDecorationBounds(Rectangle decorated) {
		// Amount we have to translate the Graphics context
		this.originOffset.x = decorated.x;
		this.originOffset.y = decorated.y;
		// If the the component is obscured (by a viewport or some
		// other means), use the painter bounds to clip to the visible
		// bounds. Doing may change the actual origin, so adjust our
		// origin offset accordingly
		Rectangle visible = getClippingRect(this.component, decorated);
		Rectangle clipRect = decorated.intersection(visible);
		if (decorated.x < visible.x) {
			this.originOffset.x += visible.x - decorated.x;
		}
		if (decorated.y < visible.y) {
			this.originOffset.y += visible.y - decorated.y;
		}
		return clipRect;
	}

	/**
	 * Return any clipping rectangle detected above the given component, in the
	 * coordinate space of the given component. The given rectangle is desired
	 * to be visible.
	 */
	private Rectangle getClippingRect(Container container, Rectangle desired) {
		Rectangle visible = container instanceof JComponent
				? ((JComponent) container).getVisibleRect()
				: new Rectangle(0, 0, container.getWidth(), container.getHeight());
		Rectangle clip = new Rectangle(desired);
		if (desired.x >= visible.x && desired.y >= visible.y
				&& desired.x + desired.width <= visible.x + visible.width
				&& desired.y + desired.height <= visible.y + visible.height) {
			// desired rect is within the current clip rect
		} else if (container.getParent() != null) {
			// Only apply the clip if it is actually smaller than the
			// component's visible area
			if (container != this.painter.getParent()
					&& (visible.x > 0 || visible.y > 0
							|| visible.width < container.getWidth()
					|| visible.height < container.getHeight())) {
				// Don't alter the original rectangle
				desired = new Rectangle(desired);
				desired.x = Math.max(desired.x, visible.x);
				desired.y = Math.max(desired.y, visible.y);
				desired.width = Math.min(desired.width,
									visible.x + visible.width - desired.x);
				desired.height = Math.min(desired.height,
									visible.y + visible.height - desired.y);

				// Check for clipping further up the hierarchy
				desired.x += container.getX();
				desired.y += container.getY();
				clip = getClippingRect(container.getParent(), desired);
				clip.x -= container.getX();
				clip.y -= container.getY();
			}
		}
		return clip;
	}

	/**
	 * Return the bounds, relative to the decorated component, of the
	 * decoration. The default covers the entire component. Note that this
	 * method will be called from the constructor, so be careful when overriding
	 * and referencing derived class state.
	 */
	protected Rectangle getDecorationBounds() {
		return this.bounds != DEFAULT_BOUNDS
				? this.bounds : new Rectangle(0, 0, this.component.getWidth(), this.component.getHeight());
	}

	/**
	 * Change the bounds of the decoration, relative to the decorated component.
	 * The special value {@link #DEFAULT_BOUNDS} means the bounds will track the
	 * component bounds.
	 */
	public void setDecorationBounds(Rectangle bounds) {
		if (bounds == DEFAULT_BOUNDS) {
			this.bounds = bounds;
		} else {
			this.bounds = new Rectangle(bounds);
		}
		synch();
	}

	/**
	 * Change the bounds of the decoration, relative to the decorated component.
	 */
	public void setDecorationBounds(int x, int y, int w, int h) {
		setDecorationBounds(new Rectangle(x, y, w, h));
	}

	protected void setPainterBounds(int x, int y, int w, int h) {
		this.painter.setBounds(x, y, w, h);
		repaint();
	}

	/** Returns the decorated component. */
	protected JComponent getComponent() {
		return this.component;
	}

	/**
	 * Returns the component used to paint the decoration and optionally track
	 * events.
	 */
	protected JComponent getPainter() {
		return this.painter;
	}

	/**
	 * Set the cursor to appear anywhere over the decoration bounds. If null,
	 * the cursor of the decorated component will be used.
	 */
	public void setCursor(Cursor cursor) {
		this.painter.setCursor(cursor);
	}

	/** Force a refresh of the underlying component and its decoration. */
	public void repaint() {
		JLayeredPane p = (JLayeredPane) this.painter.getParent();
		if (p != null) {
			p.repaint(this.painter.getBounds());
		}
	}

	/** Stop decorating. */
	public void dispose() {
		// Disposal must occur on the EDT
		if (!SwingUtilities.isEventDispatchThread()) {
			SwingUtilities.invokeLater(new Runnable() {

				@Override
				public void run() {
					dispose();
				}
			});
			return;
		}

		this.component.removeHierarchyListener(this.listener);
		this.component.removeHierarchyBoundsListener(this.listener);
		this.component.removeComponentListener(this.listener);
		if (this.parent != null) {
			this.parent.removeComponentListener(this.listener);
			this.parent = null;
		}
		if (this.layerRoot != null) {
			this.layerRoot.removePropertyChangeListener(this.listener);
			this.layerRoot = null;
		}
		Container painterParent = this.painter.getParent();
		if (painterParent != null) {
			Rectangle b = this.painter.getBounds();
			painterParent.remove(this.painter);
			painterParent.repaint(b.x, b.y, b.width, b.height);
		}
		this.component.repaint();
		this.component = null;
	}

	/**
	 * Define the decoration's appearance. The point (0,0) represents the upper
	 * left corner of the decorated component. The default clip mask will be the
	 * extents of the decoration bounds, as indicated by
	 * {@link #getDecorationBounds()}, which defaults to the decorated component
	 * bounds.
	 */
	public abstract void paint(Graphics g);

	@Override
	public String toString() {
		return super.toString() + " on " + getComponent();
	}

	private static Field nComponents;

	static {
		try {
			nComponents = Container.class.getDeclaredField("ncomponents");
			nComponents.setAccessible(true);
		} catch (Exception e) {
			nComponents = null;
		}
	}

	private static boolean useSimpleBackground() {
		return nComponents == null;
	}

	/** Used to hook into the Swing painting architecture. */
	protected class Painter extends JComponent {

		private int base;
		private Cursor cursor;

		{
			setFocusable(false);
		}

		@Override
		public boolean isShowing() {
			return getComponent().isShowing();
		}

		public JComponent getComponent() {
			return AbstractComponentDecorator.this.getComponent();
		}

		public void setDecoratedLayer(int base) {
			this.base = base;
		}

		public int getDecoratedLayer() {
			return this.base;
		}

		public boolean isBackgroundDecoration() {
			return AbstractComponentDecorator.this.layerOffset < 0;
		}

		/**
		 * Set the cursor to something else. If null, the cursor of the
		 * decorated component will be used.
		 */
		@Override
		public void setCursor(Cursor cursor) {
			Cursor oldCursor = getCursor();
			// Make sure the cursor actually changed, otherwise
			// we get cursor flicker (notably on w32 title bars)
			if (oldCursor == null && cursor != null
					|| oldCursor != null && !oldCursor.equals(cursor)) {
				this.cursor = cursor;
				super.setCursor(cursor);
			}
		}

		/**
		 * Returns the cursor of the decorated component, or the last cursor set
		 * by {@link #setCursor}.
		 */
		@Override
		public Cursor getCursor() {
			return this.cursor != null ? this.cursor : AbstractComponentDecorator.this.component.getCursor();
		}

		/**
		 * Delegate to the containing decorator to perform the paint.
		 */
		@Override
		public void paintComponent(Graphics g) {
			if (!AbstractComponentDecorator.this.component.isShowing()) {
				return;
			}
			Graphics g2 = g.create();
			g2.translate(-AbstractComponentDecorator.this.originOffset.x, -AbstractComponentDecorator.this.originOffset.y);
			AbstractComponentDecorator.this.paint(g2);
			g2.dispose();
		}

		/**
		 * Provide a decorator-specific tooltip, shown when within the
		 * decorator's bounds.
		 */
		@Override
		public String getToolTipText(MouseEvent e) {
			return AbstractComponentDecorator.this.getToolTipText();
		}

		@Override
		public String toString() {
			return "Painter for " + AbstractComponentDecorator.this;
		}
	}

	/**
	 * Provides a shared background painting mechanism for multiple decorations.
	 * This ensures that the background is only painted once if more than one
	 * background decorator is applied.
	 */
	private static class BackgroundPainter extends AbstractComponentDecorator {

		private static String key(int layer) {
			return "backgroundPainter for layer " + layer;
		}

		private String key;
		private int layer;

		public BackgroundPainter(JLayeredPane p, int layer) {
			super(p, 0, TOP);
			this.layer = layer;
			this.key = key(layer);
			p.putClientProperty(this.key, this);
			attach();
		}

		// "Hide" children by temporarily setting the component count to zero

		private int hideChildren(Container c) {
			if (c == null) {
				return 0;
			}
			int value = c.getComponentCount();
			try {
				nComponents.set(c, new Integer(0));
			} catch (Exception e) {
				return c.getComponentCount();
			}
			return value;
		}

		// Restore the child count

		private void restoreChildren(Container c, int count) {
			if (c != null) {
				try {
					nComponents.set(c, new Integer(count));
				} catch (Exception e) {
				}
			}
		}

		private void paintBackground(Graphics g, Component component, JComponent jc) {
			int x = jc.getX();
			int y = jc.getY();
			int w = jc.getWidth();
			int h = jc.getHeight();
			paintBackground(g.create(x, y, w, h), jc);
		}

		private void paintBackground(Graphics g, JComponent jc) {
			if (!jc.isShowing()) {
				return;
			}
			if (jc.isOpaque()) {
				if (useSimpleBackground()) {
					g.setColor(jc.getBackground());
					g.fillRect(0, 0, jc.getWidth(), jc.getHeight());
				} else {
					int count = hideChildren(jc);
					boolean db = jc.isDoubleBuffered();
					if (db) {
						jc.setDoubleBuffered(false);
					}
					jc.paint(g);
					if (db) {
						jc.setDoubleBuffered(true);
					}
					restoreChildren(jc, count);
				}
			}
			Component[] kids = jc.getComponents();
			for (int i = 0; i < kids.length; i++) {
				if (kids[i] instanceof JComponent) {
					paintBackground(g, jc, (JComponent) kids[i]);
				}
			}
		}

		private List<JComponent> findOpaque(Component root) {
			List<JComponent> list = new ArrayList<JComponent>();
			if (root.isOpaque() && root instanceof JComponent) {
				JComponent jc = (JComponent) root;
				list.add(jc);
				jc.setOpaque(false);
			}
			if (root instanceof Container) {
				Component[] kids = ((Container) root).getComponents();
				for (int i = 0; i < kids.length; i++) {
					list.addAll(findOpaque(kids[i]));
				}
			}
			return list;
		}

		private List<JComponent> findDoubleBuffered(Component root) {
			List<JComponent> list = new ArrayList<JComponent>();
			if (root.isDoubleBuffered() && root instanceof JComponent) {
				JComponent jc = (JComponent) root;
				list.add(jc);
				jc.setDoubleBuffered(false);
			}
			if (root instanceof Container) {
				Component[] kids = ((Container) root).getComponents();
				for (int i = 0; i < kids.length; i++) {
					list.addAll(findDoubleBuffered(kids[i]));
				}
			}
			return list;
		}

		private void paintForeground(Graphics g, JComponent jc) {
			if (!jc.isShowing()) {
				return;
			}
			List<JComponent> opaque = findOpaque(jc);
			List<JComponent> db = findDoubleBuffered(jc);
			jc.paint(g);
			for (Iterator<JComponent> i = opaque.iterator(); i.hasNext();) {
				i.next().setOpaque(true);
			}
			for (Iterator<JComponent> i = db.iterator(); i.hasNext();) {
				i.next().setDoubleBuffered(true);
			}
		}

		@Override
		public void paint(Graphics g) {

			JLayeredPane lp = (JLayeredPane) getComponent();
			Component[] kids = lp.getComponents();
			// Construct an area of the intersection of all decorators
			Area area = new Area();
			List<Painter> painters = new ArrayList<Painter>();
			List<JComponent> components = new ArrayList<JComponent>();
			for (int i = kids.length - 1; i >= 0; i--) {
				if (kids[i] instanceof Painter) {
					Painter p = (Painter) kids[i];
					if (p.isBackgroundDecoration()
							&& p.getDecoratedLayer() == this.layer) {
						painters.add(p);
						if (p.isShowing()) {
							area.add(new Area(p.getBounds()));
						}
					}
				} else if (lp.getLayer(kids[i]) == this.layer
						&& kids[i] instanceof JComponent) {
					components.add((JComponent) kids[i]);
				}
			}
			if (painters.size() == 0) {
				dispose();
				return;
			}
			if (area.isEmpty()) {
				return;
			}

			g.setClip(area);

			// Paint background for that area
			for (Iterator<JComponent> i = components.iterator(); i.hasNext();) {
				JComponent c = i.next();
				if (c.isShowing()) {
					paintBackground(g, lp, c);
				}
			}

			// Paint the bg decorators
			for (Iterator<Painter> i = painters.iterator(); i.hasNext();) {
				Painter p = i.next();
				if (p.isShowing()) {
					p.paint(g.create(p.getX(), p.getY(), p.getWidth(), p.getHeight()));
				}
			}
			// Paint foreground for the area
			for (Iterator<JComponent> i = components.iterator(); i.hasNext();) {
				JComponent c = i.next();
				if (c.isShowing()) {
					paintForeground(g.create(c.getX(), c.getY(),
									c.getWidth(), c.getHeight()), c);
				}
			}
		}

		@Override
		public void dispose() {
			getComponent().putClientProperty(this.key, null);
			super.dispose();
		}

		@Override
		public String toString() {
			return this.key + " on " + getComponent();
		}
	}

	/** Tracks changes to component configuration. */
	private final class Listener extends ComponentAdapter
			implements HierarchyListener, HierarchyBoundsListener,
			PropertyChangeListener {

		@Override
		public void hierarchyChanged(HierarchyEvent e) {
			if ((e.getChangeFlags() & HierarchyEvent.PARENT_CHANGED) != 0) {
				attach();
			}
		}

		@Override
		public void ancestorResized(HierarchyEvent e) {
			synch();
		}

		@Override
		public void ancestorMoved(HierarchyEvent e) {
			synch();
		}

		@Override
		public void propertyChange(PropertyChangeEvent e) {
			if (JLayeredPane.LAYER_PROPERTY.equals(e.getPropertyName())) {
				attach();
			}
		}

		@Override
		public void componentMoved(ComponentEvent e) {
			synch();
		}

		@Override
		public void componentResized(ComponentEvent e) {
			synch();
		}

		@Override
		public void componentHidden(ComponentEvent e) {
			setVisible(false);
		}

		@Override
		public void componentShown(ComponentEvent e) {
			setVisible(true);
		}
	}
}
